﻿using System;
using System.Collections.Generic;
using EnvDTE;
using EnvDTE80;
using EnvDTE100;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.VCProjectEngine;
using Microsoft.VisualStudio.VCCodeModel;
using System.Data.SqlServerCe;

namespace Highlighterr
{

    public enum IntellisenserType
    {
        Something = 0,  //not really used
        Function = 1,   //global function
        Type = 2,       //class, struct, interface
        Typedef = 3,    //typedef
        Enum = 4,       //enum and enum members
        Macro = 5,      //macro
    }
    public class IntellisenserItem
    {
        //this object goes into dictionaries to store info about each word
        public long ID;
        public string Name;
        public IntellisenserType Type;
    }

    public class Intellisenser
    {
        public string SolutionName;
        public DTE2 Dte2;
        public Solution SolutionObject;        
        public CodeModelEvents CodeModelEvents = null;

        private _dispCodeModelEvents_ElementAddedEventHandler CodeModelElementAddedHandler;
        private _dispCodeModelEvents_ElementChangedEventHandler CodeModelElementChangedHandler;
        private _dispCodeModelEvents_ElementDeletedEventHandler CodeModelElementDeletedHandler;

        //private List<VCProjectEngineEvents> ProjectEngineEvents = new List<VCProjectEngineEvents>();
        //private _dispVCProjectEngineEvents_ItemAddedEventHandler VCEventsItemAddedHandler;
        //private _dispVCProjectEngineEvents_ItemRemovedEventHandler VCEventsItemRemovedHandler;
        //private _dispVCProjectEngineEvents_ItemRenamedEventHandler VCEventsItemRenamedHandler;


        //using an array will hopefully provide some sort of performance increase over iterating the collection of chars..
        public static string IdentifierChars = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
        public bool[] IsIdentifierChar = new bool[256];

        //it's kind of stupid <XD
        //256 duelling dictionaries for identifiers.. array indexed by first character of identifier.
        //this could be fairly easily modified to use less than 256 dictionaries (only 63 are used..)
        private Dictionary<string, IntellisenserItem>[] _Dictionaries = new Dictionary<string, IntellisenserItem>[256];
        public string SDFFileName = string.Empty;
        public string SDFConnectionString = string.Empty;
        public int ItemsLoadedFromSDF = 0;
        public int TotalItemsInDictionary = 0;
        public long LastElementID = 0;
        public int WordsParsed = 0;

        private Dictionary<long, IntellisenserItem> _IDDictionary = new Dictionary<long, IntellisenserItem>();

        private List<string> _ErrorLog = new List<string>();
        public List<string> ErrorLog
        {
            get
            {
                return _ErrorLog;
            }
        }
        public delegate void IntellisenserLogItemAdded(string error);
        public event IntellisenserLogItemAdded OnLogItemAdded = null;

        public Intellisenser(DTE2 dte)
        {

            try
            {
                Dte2 = dte;
                if (Dte2 == null) return; //don't activate if any of these objects is null
                SolutionObject = Dte2.Solution;
                if (SolutionObject == null) return;
                SolutionName = SolutionObject.FullName;
            }
            catch (Exception ex)
            {
                System.Windows.MessageBox.Show("Highlighterr encountered an error while trying to access the current project!\n\n" + ex.Message, ":(");
            }


            //for (int i = 1; i <= SolutionObject.Projects.Count; i++)
            //{
            //    Project p = SolutionObject.Projects.Item(i);
            //    if (p == null) continue;
            //    CodeModel cm = p.CodeModel;
            //    if (cm == null) continue;
            //    VCCodeModel vccm = cm as VCCodeModel;
            //    if (vccm == null) continue;
            //    //...is this really necessary??
            //}



            AttachCodeModelEvents();


            //init the base letters array and dicts
            for (int i = 0; i < 256; i++)
            {
                IsIdentifierChar[i] = false;
                _Dictionaries[i] = new Dictionary<string, IntellisenserItem>();
                char c = (char)i;
                for (int j = 0; j < IdentifierChars.Length; j++)
                {
                    char ac = IdentifierChars[j];
                    if (c == ac)
                    {
                        IsIdentifierChar[i] = true;
                        break;
                    }
                }
            }


                



            LoadItemsFromSolutionSDF(); //this seems to work much better!



        }
        ~Intellisenser()
        {
            DetachCodeModelEvents();
        }



        private void LoadItemsFromSolutionSDF()
        {
            try
            {

                //this method opens the solution's SDF file, and loads all the relevant
                //names and types into the dictionaries.
                //this probably "is not supported" by Microsoft.
                //it picks up more words than the other method, and seems to be faster.
                //it should be detecting everything that VS intellisense picked up.


                //there's probably a better way to find the filename of the database...
                //maybe a property of the solution contains it?
                string solp = SolutionObject.Properties.Item("Path").Value;
                string dbfile = System.IO.Path.ChangeExtension(solp, "sdf");
                string dbcstring = "Data Source=" + dbfile + ";Persist Security Info=False;";
                SDFFileName = System.IO.Path.GetFileName(dbfile);
                SDFConnectionString = dbcstring;

                SqlCeConnection cn = new SqlCeConnection(dbcstring);
                cn.Open();

                //should probably query the code_item_kinds table to find these values, instead
                //of just assuming them like this...
                //classes(1), structs(2), enums(4,9?), interfaces(5), typedefs(47), 
                //functions(22), macros(33,34(un)),  [variables(23)],
                //base_class(17)
                SqlCeCommand comm = cn.CreateCommand();
                string sql = "SELECT id,name,kind FROM code_items WHERE ";
                sql += "kind=1 OR kind=2 OR kind=4 OR kind=9 OR kind=5 OR kind=47 OR kind=22 OR kind=33 OR kind=17";// OR kind=23
                
                //hmm, how to filter only current project's items?
                //maybe index items by the file they're defined in..?

                comm.CommandText = sql;
                SqlCeDataReader rdr = comm.ExecuteReader();

                //read all the matching names and types, and put them into the dictionaries.
                int rowcount = 0;
                while (rdr.Read())
                {
                    long sid = rdr.GetInt64(0);
                    string sname = rdr.GetString(1);
                    int skind = rdr.GetInt16(2);

                    switch (skind)
                    {
                        case 1://class
                        case 2://struct
                        case 5://interface
                        case 17://base class
                            AddToDicts(sid, sname, IntellisenserType.Type);
                            break;
                        case 47://typedef
                            AddToDicts(sid, sname, IntellisenserType.Typedef);
                            break;
                        case 22://function
                            //case 23://variable
                            AddToDicts(sid, sname, IntellisenserType.Function);
                            break;
                        case 33://macro
                            AddToDicts(sid, sname, IntellisenserType.Macro);
                            break;
                        case 4://enum
                        case 9://enum member
                            AddToDicts(sid, sname, IntellisenserType.Enum);
                            break;
                        default://this shouldn't happen really
                            AddToDicts(sid, sname, IntellisenserType.Something);
                            break;
                    }

                    rowcount++;

                    if (sid > LastElementID)
                    {
                        LastElementID = sid;
                    }
                }
                rdr.Close();
                rdr.Dispose();
                comm.Dispose();
                cn.Close();
                cn.Dispose();

                ItemsLoadedFromSDF = rowcount;
                WordsParsed = 0;
            }
            catch (Exception ex)
            {
                LogError(ex.ToString());
                //System.Windows.MessageBox.Show("Highlighterr encountered an error while trying to load the current solution's SDF file.\n\n" + ex.Message, ":(");
            }
            finally
            {
                //should probably handle errors here better..
                //eg. dispose connection and reader if necessary
            }
        }


        private void ClearDicts()
        {
            foreach (Dictionary<string, IntellisenserItem> d in _Dictionaries)
            {
                d.Clear();
            }
            _IDDictionary.Clear();

            ItemsLoadedFromSDF = 0;
            TotalItemsInDictionary = 0;
            LastElementID = 0;

        }
        private void AddToDicts(long id, string name, IntellisenserType type)
        {
            //this adds an item into a dictionary, if it's not in there already.
            if (!string.IsNullOrEmpty(name))
            {
                int fl = (int)name[0];
                if (fl < 256)
                {
                    IntellisenserItem i = Find(name);
                    if (i == null)
                    {
                        i = new IntellisenserItem();
                        i.ID = id;
                        i.Name = name;
                        i.Type = type;
                        _Dictionaries[fl].Add(name, i);
                        _IDDictionary.Add(id, i);
                        TotalItemsInDictionary++;
                    }
                }
            }
        }
        private void RemoveFromDicts(IntellisenserItem i)
        {
            _IDDictionary.Remove(i.ID);
            Dictionary<string, IntellisenserItem> d = GetDictionaryForName(i.Name);
            if (d != null)
            {
                if (d.Remove(i.Name))
                {
                    TotalItemsInDictionary--;
                }
            }
        }
        private void UpdateItemName(IntellisenserItem i, string newname)
        {
            Dictionary<string, IntellisenserItem> olddict = GetDictionaryForName(i.Name);
            Dictionary<string, IntellisenserItem> newdict = GetDictionaryForName(newname);
            olddict.Remove(i.Name);
            i.Name = newname;
            if (!newdict.ContainsKey(i.Name))
            {
                newdict.Add(i.Name, i);
            }
        }

        private Dictionary<string, IntellisenserItem> GetDictionaryForName(string name)
        {
            Dictionary<string, IntellisenserItem> r = null;
            if (!string.IsNullOrEmpty(name))
            {
                int fl = (int)name[0];
                if (fl < 256)
                {
                    r = _Dictionaries[fl];
                }
            }
            return r;
        }
        private IntellisenserType GetIntellisenserTypeFromElement(CodeElement2 e)
        {
            IntellisenserType r = IntellisenserType.Something;
            vsCMElement ek = e.Kind;
            CodeElement2 ep = e.Collection.Parent;
            if (ep != null)
            {
                if (ep.Kind == vsCMElement.vsCMElementEnum) //changes children of enums to enums, instead of variables.
                {
                    ek = vsCMElement.vsCMElementEnum;
                }
            }

            switch (ek)
            {
                case vsCMElement.vsCMElementClass:
                case vsCMElement.vsCMElementStruct:
                case vsCMElement.vsCMElementInterface:
                    r = IntellisenserType.Type;
                    break;
                case vsCMElement.vsCMElementTypeDef:
                    r = IntellisenserType.Typedef;
                    break;
                case vsCMElement.vsCMElementFunction:
                    r = IntellisenserType.Function;
                    break;
                case vsCMElement.vsCMElementMacro:
                    r = IntellisenserType.Macro;
                    break;
                case vsCMElement.vsCMElementEnum:
                    r = IntellisenserType.Enum;
                    break;
                default:
                    r = IntellisenserType.Something;
                    break;
            }
            return r;
        }


        public IntellisenserItem Find(string s)
        {
            //tries to pull a matching item out of the dictionaries
            //returns null if no match
            IntellisenserItem r = null;
            if (!string.IsNullOrEmpty(s))
            {
                int fl = (int)s[0];
                if (fl < 256)
                {
                    _Dictionaries[fl].TryGetValue(s, out r);
                }
            }
            WordsParsed++;
            return r;
        }
        public IntellisenserItem GetByID(long id)
        {
            IntellisenserItem r = null;
            _IDDictionary.TryGetValue(id, out r);
            return r;
        }


        public void ReloadSolutionItems()
        {
            ClearDicts();
            LoadItemsFromSolutionSDF();
        }

        public void LogError(string error)
        {
            string entry = string.Format("{0} - {1}", DateTime.Now, error);
            _ErrorLog.Add(entry);
            if (OnLogItemAdded != null)
            {
                OnLogItemAdded(entry);
            }
        }


        private void AttachCodeModelEvents()
        {

            CodeModelEvents = (CodeModelEvents)Dte2.Events.GetObject("CodeModelEvents");
            if (CodeModelEvents != null)
            {
                CodeModelElementAddedHandler = new _dispCodeModelEvents_ElementAddedEventHandler(CodeModelEvents_ElementAdded);
                CodeModelElementChangedHandler = new _dispCodeModelEvents_ElementChangedEventHandler(CodeModelEvents_ElementChanged);
                CodeModelElementDeletedHandler = new _dispCodeModelEvents_ElementDeletedEventHandler(CodeModelEvents_ElementDeleted);
                CodeModelEvents.ElementAdded += CodeModelElementAddedHandler;
                CodeModelEvents.ElementChanged += CodeModelElementChangedHandler;
                CodeModelEvents.ElementDeleted += CodeModelElementDeletedHandler;
            }


            //this might come in handy, but it doesm't seem to give what i need for now.. which is the proper item changed handler for code elements!! :(
            //VCEventsItemAddedHandler = new _dispVCProjectEngineEvents_ItemAddedEventHandler(VCEvents_ItemAdded);
            //VCEventsItemRemovedHandler = new _dispVCProjectEngineEvents_ItemRemovedEventHandler(VCEvents_ItemRemoved);
            //VCEventsItemRenamedHandler = new _dispVCProjectEngineEvents_ItemRenamedEventHandler(VCEvents_ItemRenamed);
            //for (int i = 1; i < SolutionObject.Projects.Count; i++)
            //{
            //    Project p = SolutionObject.Projects.Item(i);
            //    VCProject vcp = p.Object as VCProject;
            //    if (vcp != null)
            //    {
            //        VCProjectEngine eng = vcp.VCProjectEngine;
            //        VCProjectEngineEvents events = eng.Events;
            //        if (events != null)
            //        {
            //            if (!ProjectEngineEvents.Contains(events))
            //            {
            //                events.ItemAdded += VCEventsItemAddedHandler;
            //                events.ItemRemoved += VCEventsItemRemovedHandler;
            //                events.ItemRenamed += VCEventsItemRenamedHandler;
            //                ProjectEngineEvents.Add(events);
            //            }
            //        }
            //    }
            //}


        }
        private void DetachCodeModelEvents()
        {
            if (CodeModelEvents != null)
            {
                CodeModelEvents.ElementAdded -= CodeModelElementAddedHandler;
                CodeModelEvents.ElementChanged -= CodeModelElementChangedHandler;
                CodeModelEvents.ElementDeleted -= CodeModelElementDeletedHandler;
            }

            //foreach (VCProjectEngineEvents e in ProjectEngineEvents)
            //{
            //    e.ItemAdded -= VCEventsItemAddedHandler;
            //    e.ItemRemoved -= VCEventsItemRemovedHandler;
            //    e.ItemRenamed -= VCEventsItemRenamedHandler;
            //}
            //ProjectEngineEvents.Clear();

        }

        private void CodeModelEvents_ElementDeleted(object Parent, CodeElement Element)
        {
            if (Element != null)
            {
                CodeElement2 ce2 = Element as CodeElement2;
                IntellisenserItem item = Find(ce2.Name);// GetByID(long.Parse(ce2.ElementID));
                if (item != null)
                {
                    RemoveFromDicts(item);
                }
            }
        }
        private void CodeModelEvents_ElementChanged(CodeElement Element, vsCMChangeKind Change)
        {
            if (Element != null)
            {
                CodeElement2 ce2 = Element as CodeElement2;
                IntellisenserType type = GetIntellisenserTypeFromElement(ce2);
                if (type != IntellisenserType.Something)//make sure it's an element we care about!
                {

                    //how to remove the old element from the dictionaries without knowing its name or ID??
                    //i don't think it's possible to use the object itself as an identifier, since we
                    //might just be getting a copy of the 'real' object...

                    //until that problem is solved, we'll just add the new type name into the dicts...
                    AddToDicts(++LastElementID, ce2.Name, type);

                    //something like this is needed.. but ElementID isn't implemented!!(???!)
                    //IntellisenserItem item = GetByID(long.Parse(ce2.ElementID));
                    //if (item != null)
                    //{
                    //    item.Type = GetIntellisenserTypeFromCMElement(ce2.Kind);
                    //    UpdateItemName(item, ce2.Name);
                    //}
                }
            }
        }
        private void CodeModelEvents_ElementAdded(CodeElement Element)
        {
            if (Element != null)
            {
                CodeElement2 ce2 = Element as CodeElement2;
                //VCCodeElement vce = ce2 as VCCodeElement;

                IntellisenserType testt = GetIntellisenserTypeFromElement(ce2);
                if (testt != IntellisenserType.Something)
                {
                    AddToDicts(++LastElementID, ce2.Name, testt);
                }
            }
        }
        
        private void VCEvents_ItemAdded(object Item, object ItemParent)
        {
        }
        private void VCEvents_ItemRemoved(object Item, object ItemParent)
        {
        }
        private void VCEvents_ItemRenamed(object Item, object ItemParent, string OldName)
        {
            //can't seem to get these events to fire for code items..
        }



    }


}